package cn.com.zero.manager.gateway.filter;

import cn.com.zero.api.auth.config.properties.TokenProperties;
import cn.com.zero.api.auth.util.TokenUtil;
import cn.com.zero.api.auth.util.UserCacheKeyUtil;
import cn.com.zero.base.exception.ErrorCodeException;
import cn.com.zero.base.util.RedisUtil;
import cn.com.zero.manager.auth.api.constant.TokenKeyConstant;
import cn.com.zero.manager.gateway.enums.LoginClientEnum;
import cn.com.zero.manager.gateway.po.MenuPO;
import cn.com.zero.manager.gateway.vo.OnlineVerificationTimeVO;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author Yang Yao
 * @version 1.0.0
 * @Description 鉴权过滤器
 * @createTime 2023/2/28 16:33
 */
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

    /**
     * 输出日志对象
     */
    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    private RedisUtil redisUtil;

    /**
     * token加密密钥
     */
    @Value(TokenKeyConstant.ENCODEKEY)
    private String encodeKey;

    /**
     * token解密密钥
     */
    @Value(TokenKeyConstant.DECODEKEY)
    private String decodeKey;

    /**
     * token有效时间
     */
    @Value(TokenKeyConstant.EXPIRETIME)
    private String expireTime;

    /**
     * token刷新次数上限
     */
    @Value(TokenKeyConstant.REFRESHTIMES)
    private String refreshTimes;

    /**
     * token签名加密算法
     */
    @Value(TokenKeyConstant.SIGNATUREALGORITHM)
    private String signatureAlgorithm;

    /**
     * token刷新时段
     */
    @Value(TokenKeyConstant.REFRESHINTERVAL)
    private String refreshInterval;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //获取请求头
        HttpHeaders headers = request.getHeaders();
        String userId;
        String token;
        String loginClient;
        // 前端请求服务的路径
        String path = request.getURI().getPath();
        // 去掉第一个/
        path = path.substring(1);
        // 去掉路径中的服务名，直接找权限路径url
        path = path.substring(path.indexOf("/"));

        // 校验请求路径是否属于公共权限路径
        if (checkPath(path)) return chain.filter(exchange);
        //请求头中获取令牌
        userId = headers.getFirst(TokenKeyConstant.USERID);
        token = headers.getFirst(TokenKeyConstant.TOKEN);
        loginClient = headers.getFirst(TokenKeyConstant.LOGINCLIENT);
        if (StrUtil.isBlank(userId) || StrUtil.isBlank(token) || StrUtil.isBlank(loginClient)) {
            logger.error("请求参数非法,userId[{}],token[{}],loginClient[{}]", userId, token, loginClient);
            throw new ErrorCodeException(-999998);
        }
        loginClient = LoginClientEnum.getValueByName(Integer.parseInt(loginClient));
        // 检验token
        checkToken(token, response, userId, loginClient);

        if (checkIsPermission(path, userId, loginClient)) return chain.filter(exchange);
        throw new ErrorCodeException(-999990L);
    }

    /**
     * 检查该用户是否具有访问此路径的权限信息
     *
     * @param path   请求路径
     * @param userid 用户ID
     * @return true-具有权限 false-无权访问
     */
    private boolean checkIsPermission(String path, String userid, String loginClient) {

        // 获取用户只要登录了就有的权限列表
        List<String> selectPublicPermissionIsLogin = (List<String>) redisUtil.getObject(UserCacheKeyUtil.getPublicLoginPermissionCacheKey(TokenKeyConstant.MANAGE_MS_KEY));
        if (selectPublicPermissionIsLogin.contains(path)) {
            return true;
        }

        // 从redis中获取该用户的权限信息 auth：代表权限对象集合List<MenuPO>的字符串
        String auth = (String) redisUtil.getObject(UserCacheKeyUtil.getMenuPermissionCacheKey(TokenKeyConstant.MANAGE_MS_KEY, loginClient, userid));
        // 将其转化为JSONArray对象
        JSONArray jsonArray = JSONArray.parseArray(auth);
        // 然后将jsonArray转化为List<MenuPO>集合
        List<MenuPO> menuPOS = jsonArray.toJavaList(MenuPO.class);
        menuPOS.removeIf(t -> {
            if (StrUtil.isBlank(t.getUrl())) {
                return true;
            }
            return false;
        });
        List<String> permission = new ArrayList<>();
        for (MenuPO menuPO : menuPOS) {
            permission.add(menuPO.getUrl());
        }
        for (String url : permission) {
            if (url.lastIndexOf("*") != -1) {
                String regex = path.substring(0, path.lastIndexOf("/") + 1);
                String partUrl = url.substring(0, url.lastIndexOf("/") + 1);
                if (partUrl.equals(regex)) {
                    return true;
                }
            } else {
                if (url.equals(path)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 创建TokenProperties对象
     *
     * @param userId 管理员Id
     * @return
     */
    private TokenProperties setTokenProperties(String userId) {
        TokenProperties tokenProperties = new TokenProperties();
        tokenProperties.setIssuer(userId);
        tokenProperties.setEncodeKey(encodeKey);
        tokenProperties.setDecodeKey(decodeKey);
        tokenProperties.setExpireTime(Long.valueOf(expireTime));
        tokenProperties.setRefreshInterval(Long.valueOf(refreshInterval));
        tokenProperties.setRefreshTimes(Integer.valueOf(refreshTimes));
        tokenProperties.setSignatureAlgorithm(signatureAlgorithm);
        return tokenProperties;
    }

    /**
     * 校验请求头中的token信息，及获取用户ID
     *
     * @param token       token信息
     * @param response    响应体
     * @param userId      用户代码
     * @param loginClient 登录设备
     * @return 用户ID
     */
    private void checkToken(String token, ServerHttpResponse response, String userId, String loginClient) {
        if (StrUtil.isBlank(token)) {
            throw new ErrorCodeException(-999990L);
        }
        //获取token对象
        TokenUtil tokenUtil = new TokenUtil(setTokenProperties(userId));
        // 从redis中获取当前用户信息的key
        String onlineUserCacheKey = UserCacheKeyUtil.getOnlineUserCacheKey(TokenKeyConstant.MANAGE_MS_KEY, loginClient, userId);
        // 获取当前用户信息
        String online = (String) redisUtil.getObject(onlineUserCacheKey);
        OnlineVerificationTimeVO onlineManagerVO = JSONObject.parseObject(online, OnlineVerificationTimeVO.class);
        String tokenSubject = tokenUtil.getTokenSubject(token, String.class);
        if (null == onlineManagerVO) {
            logger.error("登录已超时,onlineManagerVO:{}", onlineManagerVO);
            throw new ErrorCodeException(-80002);
        }
        if (!userId.equals(tokenSubject)) {
            logger.error("请求参数非法,tokenSubject:{},userId:{}", tokenSubject, userId);
            throw new ErrorCodeException(-999998);
        }
        Date currentTimeMillis = new Date(System.currentTimeMillis());
        String refreshToken = tokenUtil.checkAndRefreshToken(token, currentTimeMillis, onlineManagerVO.getTokenCreateTime(), String.class);
        if (!token.equals(refreshToken)) {
            onlineManagerVO.setTokenCreateTime(currentTimeMillis);
            redisUtil.setObjectAndTime(onlineUserCacheKey, JSONObject.toJSONString(onlineManagerVO),
                    Long.parseLong(expireTime), TimeUnit.SECONDS);
            HttpHeaders responseHeaders = response.getHeaders();
            responseHeaders.set("token", refreshToken);
        }
    }

    /**
     * 校验请求路径是否属于公共权限路径
     *
     * @param path
     * @return
     */
    private boolean checkPath(String path) {
        //获取公共权限
        List<String> publicPermissionPOS = (List<String>) redisUtil.getObject(UserCacheKeyUtil.getPublicPermissionCacheKey(TokenKeyConstant.MANAGE_MS_KEY));
        if (publicPermissionPOS.contains(path)) {
            return true;
        }
        return false;
    }


    @Override
    public int getOrder() {
        return 0;
    }

    @Autowired
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }
}
